package ldap

import (
	"context"
	"crypto/tls"
	"fmt"

	"github.com/go-ldap/ldap/v3"
	"github.com/pingcap/log"
	"go.uber.org/fx"

	"github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/utils"

	"github.com/pingcap-incubator/tidb-dashboard/pkg/config"
)

var (
	ErrLDAPConnFailed = ErrNS.NewType("ldap_conn_failed")
	ErrLDAPAuthFailed = ErrNS.NewType("ldap_auth_failed")
)

const (
	DefaultLDAPUrl  = "10.204.243.21:389"
	LdapAttrID      = "sAMAccountName"
	LdapAttrName    = "sn"
	LdapAttrTel     = "telephoneNumber"
	LdapAttrMail    = "mail"
	LdapDescription = "description"
)

type Client struct {
	lifecycleCtx context.Context
	ldapURL      string
}

type User struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	Phone       string `json:"phone"`
	Mail        string `json:"mail"`
	Description string `json:"description"`
}

func NewLDAPClient(lc fx.Lifecycle, config *config.Config) *Client {
	ldapURL := DefaultLDAPUrl
	if config.LdapURL != "" {
		ldapURL = config.LdapURL
	}

	client := &Client{
		lifecycleCtx: nil,
		ldapURL:      ldapURL,
	}

	lc.Append(fx.Hook{
		OnStart: func(ctx context.Context) error {
			client.lifecycleCtx = ctx
			return nil
		},
	})

	return client
}

func (c *Client) bindUser(username string, password string) (*ldap.Conn, error) {
	l, err := ldap.Dial("tcp", c.ldapURL)
	if err != nil {
		log.Warn(err.Error())
		return nil, ErrLDAPConnFailed.Wrap(err, "Connect failed")
	}

	// Reconnect with TLS
	err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) // #nosec G402
	if err != nil {
		log.Warn(err.Error())
		return nil, ErrLDAPConnFailed.Wrap(err, "Reconnect with TLS failed")
	}

	// First bind with a read only user
	err = l.Bind("fcbox\\"+username, password)
	if err != nil {
		log.Warn(err.Error())
		return nil, ErrLDAPAuthFailed.Wrap(err, "Wrong username or password")
	}
	return l, nil
}

func (c *Client) buildSingleParamRequest(paramKey string, paramValue string) *ldap.SearchRequest {
	// 这里是 basedn,我们将从这个节点开始搜索
	// 这里几个参数分别是 scope, derefAliases, sizeLimit, timeLimit, typesOnly
	// 详情可以参考 RFC4511 中的定义
	// 这里是 LDAP 查询的 Filter
	// 这里是查询返回的属性,以数组形式提供.如果为空则会返回所有的属性
	searchRequest := ldap.NewSearchRequest(

		"OU=Fcbox Users,DC=fcbox,DC=com",

		ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,

		fmt.Sprintf("(&(objectClass=person)(%s=%s))", paramKey, paramValue),

		[]string{},

		nil,
	)
	return searchRequest
}

func (c *Client) parseToLdapUser(attributes []*ldap.EntryAttribute) User {
	var user User
	for _, v := range attributes {
		if v.Name == LdapAttrID {
			user.ID = v.Values[0]
		}
		if v.Name == LdapAttrName {
			user.Name = v.Values[0]
		}
		if v.Name == LdapAttrTel {
			user.Phone = v.Values[0]
		}
		if v.Name == LdapAttrMail {
			user.Mail = v.Values[0]
		}
		if v.Name == LdapDescription {
			user.Description = v.Values[0]
		}
	}
	return user
}

func (c *Client) queryUserInfo(username string, password string, key string, param string, fuzzy bool) ([]User, error) {
	l, err := c.bindUser(username, password)
	if err != nil {
		return nil, err
	}
	defer l.Close()

	if fuzzy {
		param = "*" + param + "*"
	}

	searchRequest := c.buildSingleParamRequest(key, param)

	sr, err := l.Search(searchRequest)
	if err != nil {
		log.Warn(err.Error())
		return nil, ErrLDAPAuthFailed.Wrap(err, "Search user failed")
	}

	userList := make([]User, 0)
	for _, entry := range sr.Entries {
		user := c.parseToLdapUser(entry.Attributes)
		userList = append(userList, user)
	}

	return userList, nil
}

func (c *Client) GetUserInfo(username string, password string) (*User, error) {
	users, err := c.queryUserInfo(username, password, LdapAttrID, username, false)
	if err != nil {
		log.Warn(err.Error())
		return nil, ErrLDAPAuthFailed.Wrap(err, "Search user failed")
	}
	// 如果没有数据返回或者超过1条数据返回,这对于用户认证而言都是不允许的.
	// 前这意味着没有查到用户,后者意味着存在重复数据
	if len(users) != 1 {
		log.Warn("User does not exist or too many entries returned")
		return nil, ErrLDAPAuthFailed.New("User does not exist or too many entries returned")
	}
	return &users[0], nil
}

func (c *Client) QueryUserInfoByName(username string, password string, targetName string) ([]User, error) {
	return c.queryUserInfo(username, password, LdapAttrName, targetName, false)
}

func (c *Client) QueryUserInfoByUserID(username string, password string, userID string) (*User, error) {
	userList, err := c.queryUserInfo(username, password, LdapAttrID, userID, false)
	if err != nil {
		return nil, err
	}
	if len(userList) == 0 {
		return nil, nil
	}
	return &userList[0], nil
}

func (c *Client) SearchUserInfo(username string, password string, searchStr string) ([]User, error) {
	var userList []User
	var err error
	if utils.IsNumber(searchStr) {
		//是否按ID搜索
		userList, err = c.queryUserInfo(username, password, LdapAttrID, searchStr, true)
	} else {
		//按description搜索，xxx-技术研发部/基础平台处/中间件组
		userList, err = c.queryUserInfo(username, password, LdapDescription, searchStr, true)
	}
	return userList, err
}
